// (c) Copyright 2013 Mike Riedel.
// This source is subject to the Microsoft Public License (Ms-PL).
// All other rights reserved.

using System;

namespace FreshMeat.ErrorHandlingBlock
{
   /// <summary>
   /// A retry play that waits some time after the first failure before retrying and 
   /// then waits exponentially longer after every subsequent failure before retrying.
   /// </summary>
   public class BackoffRetryPlan : IRetryPlan
   {
      readonly int maxAttempts;
      readonly TimeSpan firstWait;
      readonly TimeSpan secondWait;
      readonly TimeSpan maxWait;
      readonly double multiplier;

      /// <summary>
      /// Creates instance using specified parameters
      /// </summary>
      /// <param name="maxAttempts">Maximum number of attempts.</param>
      /// <param name="firstWait">Time to wait after first failure.</param>
      /// <param name="secondWait">Time to wait after second failure.</param>
      /// <param name="maxWait">Maximum time to wait.</param>
      /// <param name="multiplier">Muliplier combined with previous wait time to calculate waits after the second failure.
      /// For example, setting this to 2 with a secondWait of 2 results in 2, 4, 8, 16, 32 and on up to the max wait.</param>
      public BackoffRetryPlan(int maxAttempts, TimeSpan firstWait, TimeSpan secondWait, TimeSpan maxWait, double multiplier)
      {
         this.maxAttempts = maxAttempts;
         this.firstWait = firstWait;
         this.secondWait = secondWait;
         this.maxWait = maxWait;
         this.multiplier = multiplier;
      }

      /// <summary>
      /// Determine whether to retry based on the number of attempts.
      /// </summary>
      /// <param name="attempts">The number of attempts so far, including the initial attempt.</param>
      /// <returns>True to retry, false to fail.</returns>
      public bool ShouldRetry(int attempts)
      {
         return attempts < maxAttempts;
      }

      /// <summary>
      /// Gets the time until the next attempt should be made.
      /// </summary>
      /// <param name="attempts">The number of attempts so far, including the initial attempt.</param>
      /// <returns>Time until the next attempt should be made.</returns>
      public TimeSpan GetTimeUntilNextAttempt(int attempts)
      {
         if (attempts <= 1)
            return firstWait;
         if (attempts == 2)
            return secondWait;

         var realMultiplier = Math.Pow(multiplier, attempts - 2);

         var result = realMultiplier * secondWait.Ticks;
         if (result >= maxWait.Ticks)
            result = maxWait.Ticks;

         return TimeSpan.FromTicks((long)result);
      }
   }
}